#include <jni.h>

//
// Created by Andrea Petreti on 27/10/2021.
//
#include <android/native_window.h>
#include <android/native_window_jni.h>
#include <android/bitmap.h>

#include <fpdfview.h>
#include <fpdf_doc.h>
#include "DocumentFile.h"
#include "pdfium-internal.h"

extern "C"
JNIEXPORT jlong JNICALL
Java_io_github_petretiandrea_pdfium_PdfiumCore_nativeOpenDocument(JNIEnv *env, jobject thiz,
                                                                  jint fd, jstring password) {
    size_t fileLength = (size_t) getFileSize(fd);
    if (fileLength <= 0) {
        jniThrowException(env, "java/io/IOException",
                          "File is empty");
        return -1;
    }

    DocumentFile *docFile = new DocumentFile();

    FPDF_FILEACCESS loader;
    loader.m_FileLen = fileLength;
    loader.m_Param = reinterpret_cast<void *>(intptr_t(fd));
    loader.m_GetBlock = &getBlock;

    const char *cpassword = nullptr;
    if (password != nullptr) {
        cpassword = env->GetStringUTFChars(password, nullptr);
    }

    FPDF_DOCUMENT document = FPDF_LoadCustomDocument(&loader, cpassword);

    if (cpassword != nullptr) {
        env->ReleaseStringUTFChars(password, cpassword);
    }

    if (!document) {
        delete docFile;

        const long errorNum = FPDF_GetLastError();
        if (errorNum == FPDF_ERR_PASSWORD) {
            jniThrowException(env, "com/shockwave/pdfium/PdfPasswordException",
                              "Password required or incorrect password.");
        } else {
            char *error = getErrorDescription(errorNum);
            jniThrowExceptionFmt(env, "java/io/IOException",
                                 "cannot create document: %s", error);

            free(error);
        }

        return -1;
    }

    docFile->pdfDocument = document;

    return reinterpret_cast<jlong>(docFile);
}
extern "C"
JNIEXPORT jlong JNICALL
Java_io_github_petretiandrea_pdfium_PdfiumCore_nativeOpenMemDocument(JNIEnv *env, jobject thiz,
                                                                     jbyteArray data,
                                                                     jstring password) {
    auto *docFile = new DocumentFile();

    const char *cpassword = nullptr;
    if (password != nullptr) {
        cpassword = env->GetStringUTFChars(password, nullptr);
    }

    jbyte *cData = env->GetByteArrayElements(data, nullptr);
    int size = (int) env->GetArrayLength(data);
    jbyte *cDataCopy = new jbyte[size];
    memcpy(cDataCopy, cData, size);
    FPDF_DOCUMENT document = FPDF_LoadMemDocument(reinterpret_cast<const void *>(cDataCopy),
                                                  size, cpassword);
    env->ReleaseByteArrayElements(data, cData, JNI_ABORT);

    if (cpassword != nullptr) {
        env->ReleaseStringUTFChars(password, cpassword);
    }

    if (!document) {
        delete docFile;

        const long errorNum = FPDF_GetLastError();
        if (errorNum == FPDF_ERR_PASSWORD) {
            jniThrowException(env, "com/shockwave/pdfium/PdfPasswordException",
                              "Password required or incorrect password.");
        } else {
            char *error = getErrorDescription(errorNum);
            jniThrowExceptionFmt(env, "java/io/IOException",
                                 "cannot create document: %s", error);

            free(error);
        }

        return -1;
    }

    docFile->pdfDocument = document;

    return reinterpret_cast<jlong>(docFile);
}
extern "C"
JNIEXPORT void JNICALL
Java_io_github_petretiandrea_pdfium_PdfiumCore_nativeCloseDocument(JNIEnv *env, jobject thiz,
                                                                   jlong doc_ptr) {
    auto *doc = reinterpret_cast<DocumentFile *>(doc_ptr);
    delete doc;
}
extern "C"
JNIEXPORT jint JNICALL
Java_io_github_petretiandrea_pdfium_PdfiumCore_nativeGetPageCount(JNIEnv *env, jobject thiz,
                                                                  jlong doc_ptr) {
    auto *doc = reinterpret_cast<DocumentFile*>(doc_ptr);
    return (jint)FPDF_GetPageCount(doc->pdfDocument);
}

extern "C"
JNIEXPORT jlong JNICALL
Java_io_github_petretiandrea_pdfium_PdfiumCore_nativeLoadPage(JNIEnv *env, jobject thiz,
                                                              jlong doc_ptr, jint page_index) {
    auto *doc = reinterpret_cast<DocumentFile *>(doc_ptr);
    return loadPageInternal(env, doc, (int) page_index);
}
extern "C"
JNIEXPORT jlongArray JNICALL
Java_io_github_petretiandrea_pdfium_PdfiumCore_nativeLoadPages(JNIEnv *env, jobject thiz,
                                                               jlong doc_ptr, jint from_index,
                                                               jint to_index) {
    auto *doc = reinterpret_cast<DocumentFile *>(doc_ptr);

    if (to_index < from_index) return nullptr;
    jlong pages[to_index - from_index + 1];

    int i;
    for (i = 0; i <= (to_index - from_index); i++) {
        pages[i] = loadPageInternal(env, doc, (int) (i + from_index));
    }

    jlongArray javaPages = env->NewLongArray((jsize) (to_index - from_index + 1));
    env->SetLongArrayRegion(javaPages, 0, (jsize) (to_index - from_index + 1), (const jlong *) pages);

    return javaPages;
}
extern "C"
JNIEXPORT void JNICALL
Java_io_github_petretiandrea_pdfium_PdfiumCore_nativeClosePage(JNIEnv *env, jobject thiz,
                                                               jlong page_ptr) {
    closePageInternal(page_ptr);
}
extern "C"
JNIEXPORT void JNICALL
Java_io_github_petretiandrea_pdfium_PdfiumCore_nativeClosePages(JNIEnv *env, jobject thiz,
                                                                jlongArray pages_ptr) {
    int length = (int) (env->GetArrayLength(pages_ptr));
    jlong *pages = env->GetLongArrayElements(pages_ptr, nullptr);

    int i;
    for (i = 0; i < length; i++) { closePageInternal(pages[i]); }
}
extern "C"
JNIEXPORT jint JNICALL
Java_io_github_petretiandrea_pdfium_PdfiumCore_nativeGetPageWidthPixel(JNIEnv *env, jobject thiz,
                                                                       jlong page_ptr, jint dpi) {
    auto page = reinterpret_cast<FPDF_PAGE>(page_ptr);
    return (jint) (FPDF_GetPageWidth(page) * dpi / 72);
}
extern "C"
JNIEXPORT jint JNICALL
Java_io_github_petretiandrea_pdfium_PdfiumCore_nativeGetPageHeightPixel(JNIEnv *env, jobject thiz,
                                                                        jlong page_ptr, jint dpi) {
    auto page = reinterpret_cast<FPDF_PAGE>(page_ptr);
    return (jint) (FPDF_GetPageHeight(page) * dpi / 72);
}
extern "C"
JNIEXPORT jint JNICALL
Java_io_github_petretiandrea_pdfium_PdfiumCore_nativeGetPageWidthPoint(JNIEnv *env, jobject thiz,
                                                                       jlong page_ptr) {
    auto page = reinterpret_cast<FPDF_PAGE>(page_ptr);
    return (jint) FPDF_GetPageWidth(page);
}
extern "C"
JNIEXPORT jint JNICALL
Java_io_github_petretiandrea_pdfium_PdfiumCore_nativeGetPageHeightPoint(JNIEnv *env, jobject thiz,
                                                                        jlong page_ptr) {
    auto page = reinterpret_cast<FPDF_PAGE>(page_ptr);
    return (jint) FPDF_GetPageHeight(page);
}
extern "C"
JNIEXPORT void JNICALL
Java_io_github_petretiandrea_pdfium_PdfiumCore_nativeRenderPage(JNIEnv *env, jobject thiz,
                                                                jlong page_ptr, jobject surface,
                                                                jint dpi, jint start_x,
                                                                jint start_y, jint draw_size_hor,
                                                                jint draw_size_ver,
                                                                jboolean render_annot) {
    ANativeWindow *nativeWindow = ANativeWindow_fromSurface(env, surface);
    if (nativeWindow == nullptr) {
        LOGE("native window pointer nullptr");
        return;
    }
    FPDF_PAGE page = reinterpret_cast<FPDF_PAGE>(page_ptr);

    if (page == nullptr || nativeWindow == nullptr) {
        LOGE("Render page pointers invalid");
        return;
    }

    if (ANativeWindow_getFormat(nativeWindow) != WINDOW_FORMAT_RGBA_8888) {
        LOGD("Set format to RGBA_8888");
        ANativeWindow_setBuffersGeometry(nativeWindow,
                                         ANativeWindow_getWidth(nativeWindow),
                                         ANativeWindow_getHeight(nativeWindow),
                                         WINDOW_FORMAT_RGBA_8888);
    }

    ANativeWindow_Buffer buffer;
    int ret;
    if ((ret = ANativeWindow_lock(nativeWindow, &buffer, nullptr)) != 0) {
        LOGE("Locking native window failed: %s", strerror(ret * -1));
        return;
    }

    renderPageInternal(page, &buffer,
                       (int) start_x, (int) start_y,
                       buffer.width, buffer.height,
                       (int) draw_size_hor, (int) draw_size_ver,
                       (bool) render_annot);

    ANativeWindow_unlockAndPost(nativeWindow);
    ANativeWindow_release(nativeWindow);
}
extern "C"
JNIEXPORT void JNICALL
Java_io_github_petretiandrea_pdfium_PdfiumCore_nativeRenderPageBitmap(JNIEnv *env, jobject thiz,
                                                                      jlong page_ptr,
                                                                      jobject bitmap, jint dpi,
                                                                      jint start_x, jint start_y,
                                                                      jint draw_size_hor,
                                                                      jint draw_size_ver,
                                                                      jboolean render_annot) {
    auto page = reinterpret_cast<FPDF_PAGE>(page_ptr);

    if (page == nullptr || bitmap == nullptr) {
        LOGE("Render page pointers invalid");
        return;
    }

    AndroidBitmapInfo info;
    int ret;
    if ((ret = AndroidBitmap_getInfo(env, bitmap, &info)) < 0) {
        LOGE("Fetching bitmap info failed: %s", strerror(ret * -1));
        return;
    }

    int canvasHorSize = info.width;
    int canvasVerSize = info.height;

    if (info.format != ANDROID_BITMAP_FORMAT_RGBA_8888 &&
        info.format != ANDROID_BITMAP_FORMAT_RGB_565) {
        LOGE("Bitmap format must be RGBA_8888 or RGB_565");
        return;
    }

    void *addr;
    if ((ret = AndroidBitmap_lockPixels(env, bitmap, &addr)) != 0) {
        LOGE("Locking bitmap failed: %s", strerror(ret * -1));
        return;
    }

    void *tmp;
    int format;
    int sourceStride;
    if (info.format == ANDROID_BITMAP_FORMAT_RGB_565) {
        tmp = malloc(canvasVerSize * canvasHorSize * sizeof(rgb));
        sourceStride = canvasHorSize * sizeof(rgb);
        format = FPDFBitmap_BGR;
    } else {
        tmp = addr;
        sourceStride = info.stride;
        format = FPDFBitmap_BGRA;
    }

    FPDF_BITMAP pdfBitmap = FPDFBitmap_CreateEx(canvasHorSize, canvasVerSize,
                                                format, tmp, sourceStride);

    /*LOGD("Start X: %d", startX);
    LOGD("Start Y: %d", startY);
    LOGD("Canvas Hor: %d", canvasHorSize);
    LOGD("Canvas Ver: %d", canvasVerSize);
    LOGD("Draw Hor: %d", drawSizeHor);
    LOGD("Draw Ver: %d", drawSizeVer);*/

    if (draw_size_hor < canvasHorSize || draw_size_ver < canvasVerSize) {
        FPDFBitmap_FillRect(pdfBitmap, 0, 0, canvasHorSize, canvasVerSize,
                            0x848484FF); //Gray
    }

    int baseHorSize = (canvasHorSize < draw_size_hor) ? canvasHorSize : (int) draw_size_hor;
    int baseVerSize = (canvasVerSize < draw_size_ver) ? canvasVerSize : (int) draw_size_ver;
    int baseX = (start_x < 0) ? 0 : (int) start_x;
    int baseY = (start_y < 0) ? 0 : (int) start_y;
    int flags = FPDF_REVERSE_BYTE_ORDER;

    if (render_annot) {
        flags |= FPDF_ANNOT;
    }

    FPDFBitmap_FillRect(pdfBitmap, baseX, baseY, baseHorSize, baseVerSize,
                        0xFFFFFFFF); //White

    FPDF_RenderPageBitmap(pdfBitmap, page,
                          start_x, start_y,
                          (int) draw_size_hor, (int) draw_size_ver,
                          0, flags);

    if (info.format == ANDROID_BITMAP_FORMAT_RGB_565) {
        rgbBitmapTo565(tmp, sourceStride, addr, &info);
        free(tmp);
    }

    AndroidBitmap_unlockPixels(env, bitmap);
}
extern "C"
JNIEXPORT jstring JNICALL
Java_io_github_petretiandrea_pdfium_PdfiumCore_nativeGetDocumentMetaText(JNIEnv *env, jobject thiz,
                                                                         jlong doc_ptr,
                                                                         jstring tag) {
    const char *ctag = env->GetStringUTFChars(tag, nullptr);
    if (ctag == nullptr) {
        return env->NewStringUTF("");
    }
    auto *doc = reinterpret_cast<DocumentFile *>(doc_ptr);

    size_t bufferLen = FPDF_GetMetaText(doc->pdfDocument, ctag, nullptr, 0);
    if (bufferLen <= 2) {
        return env->NewStringUTF("");
    }
    std::wstring text;
    FPDF_GetMetaText(doc->pdfDocument, ctag, WriteInto(&text, bufferLen + 1), bufferLen);
    env->ReleaseStringUTFChars(tag, ctag);
    return env->NewString((jchar *) text.c_str(), bufferLen / 2 - 1);
}
extern "C"
JNIEXPORT jobject JNICALL
Java_io_github_petretiandrea_pdfium_PdfiumCore_nativeGetFirstChildBookmark(JNIEnv *env,
                                                                           jobject thiz,
                                                                           jlong doc_ptr,
                                                                           jobject bookmark_ptr) {
    auto *doc = reinterpret_cast<DocumentFile *>(doc_ptr);
    FPDF_BOOKMARK parent;
    if (bookmark_ptr == nullptr) {
        parent = nullptr;
    } else {
        jclass longClass = env->GetObjectClass(bookmark_ptr);
        jmethodID longValueMethod = env->GetMethodID(longClass, "longValue", "()J");

        jlong ptr = env->CallLongMethod(bookmark_ptr, longValueMethod);
        parent = reinterpret_cast<FPDF_BOOKMARK>(ptr);
    }
    FPDF_BOOKMARK bookmark = FPDFBookmark_GetFirstChild(doc->pdfDocument, parent);
    if (bookmark == nullptr) {
        return nullptr;
    }
    return NewLong(env, reinterpret_cast<jlong>(bookmark));
}
extern "C"
JNIEXPORT jobject JNICALL
Java_io_github_petretiandrea_pdfium_PdfiumCore_nativeGetSiblingBookmark(JNIEnv *env, jobject thiz,
                                                                        jlong doc_ptr,
                                                                        jlong bookmark_ptr) {
    auto *doc = reinterpret_cast<DocumentFile *>(doc_ptr);
    auto parent = reinterpret_cast<FPDF_BOOKMARK>(bookmark_ptr);
    FPDF_BOOKMARK bookmark = FPDFBookmark_GetNextSibling(doc->pdfDocument, parent);
    if (bookmark == nullptr) {
        return nullptr;
    }
    return NewLong(env, reinterpret_cast<jlong>(bookmark));
}
extern "C"
JNIEXPORT jstring JNICALL
Java_io_github_petretiandrea_pdfium_PdfiumCore_nativeGetBookmarkTitle(JNIEnv *env, jobject thiz,
                                                                      jlong bookmark_ptr) {
    auto bookmark = reinterpret_cast<FPDF_BOOKMARK>(bookmark_ptr);
    size_t bufferLen = FPDFBookmark_GetTitle(bookmark, nullptr, 0);
    if (bufferLen <= 2) {
        return env->NewStringUTF("");
    }
    std::wstring title;
    FPDFBookmark_GetTitle(bookmark, WriteInto(&title, bufferLen + 1), bufferLen);
    return env->NewString((jchar *) title.c_str(), bufferLen / 2 - 1);

}
extern "C"
JNIEXPORT jlong JNICALL
Java_io_github_petretiandrea_pdfium_PdfiumCore_nativeGetBookmarkDestIndex(JNIEnv *env, jobject thiz,
                                                                          jlong doc_ptr,
                                                                          jlong bookmark_ptr) {
    auto *doc = reinterpret_cast<DocumentFile *>(doc_ptr);
    auto bookmark = reinterpret_cast<FPDF_BOOKMARK>(bookmark_ptr);

    FPDF_DEST dest = FPDFBookmark_GetDest(doc->pdfDocument, bookmark);
    if (dest == nullptr) {
        return -1;
    }

    return (jlong) FPDFDest_GetDestPageIndex(doc->pdfDocument, dest);
}
extern "C"
JNIEXPORT jobject JNICALL
Java_io_github_petretiandrea_pdfium_PdfiumCore_nativeGetPageSizeByIndex(JNIEnv *env, jobject thiz,
                                                                        jlong doc_ptr,
                                                                        jint page_index, jint dpi) {
    auto *doc = reinterpret_cast<DocumentFile *>(doc_ptr);
    if (doc == nullptr) {
        LOGE("Document is nullptr");

        jniThrowException(env, "java/lang/IllegalStateException",
                          "Document is nullptr");
        return nullptr;
    }

    double width, height;
    int result = FPDF_GetPageSizeByIndex(doc->pdfDocument, page_index, &width, &height);

    if (result == 0) {
        width = 0;
        height = 0;
    }

    jint widthInt = (jint) (width * dpi / 72);
    jint heightInt = (jint) (height * dpi / 72);

    jclass clazz = env->FindClass("io/github/petretiandrea/pdfium/util/Size");
    jmethodID constructorID = env->GetMethodID(clazz, "<init>", "(II)V");
    return env->NewObject(clazz, constructorID, widthInt, heightInt);
}
extern "C"
JNIEXPORT jlongArray JNICALL
Java_io_github_petretiandrea_pdfium_PdfiumCore_nativeGetPageLinks(JNIEnv *env, jobject thiz,
                                                                  jlong page_ptr) {
    auto page = reinterpret_cast<FPDF_PAGE>(page_ptr);
    int pos = 0;
    std::vector<jlong> links;
    FPDF_LINK link;
    while (FPDFLink_Enumerate(page, &pos, &link)) {
        links.push_back(reinterpret_cast<jlong>(link));
    }

    jlongArray result = env->NewLongArray(links.size());
    env->SetLongArrayRegion(result, 0, links.size(), &links[0]);
    return result;
}
extern "C"
JNIEXPORT jobject JNICALL
Java_io_github_petretiandrea_pdfium_PdfiumCore_nativeGetDestPageIndex(JNIEnv *env, jobject thiz,
                                                                      jlong doc_ptr,
                                                                      jlong link_ptr) {
    auto *doc = reinterpret_cast<DocumentFile *>(doc_ptr);
    auto link = reinterpret_cast<FPDF_LINK>(link_ptr);
    FPDF_DEST dest = FPDFLink_GetDest(doc->pdfDocument, link);
    if (dest == nullptr) {
        return nullptr;
    }
    unsigned long index = FPDFDest_GetDestPageIndex(doc->pdfDocument, dest);
    return NewInteger(env, (jint) index);
}
extern "C"
JNIEXPORT jobject JNICALL
Java_io_github_petretiandrea_pdfium_PdfiumCore_nativeGetLinkRect(JNIEnv *env, jobject thiz,
                                                                 jlong link_ptr) {
    auto link = reinterpret_cast<FPDF_LINK>(link_ptr);
    FS_RECTF fsRectF;
    FPDF_BOOL result = FPDFLink_GetAnnotRect(link, &fsRectF);

    if (!result) {
        return nullptr;
    }

    jclass clazz = env->FindClass("android/graphics/RectF");
    jmethodID constructorID = env->GetMethodID(clazz, "<init>", "(FFFF)V");
    return env->NewObject(clazz, constructorID, fsRectF.left, fsRectF.top, fsRectF.right,
                          fsRectF.bottom);
}
extern "C"
JNIEXPORT jstring JNICALL
Java_io_github_petretiandrea_pdfium_PdfiumCore_nativeGetLinkURI(JNIEnv *env, jobject thiz,
                                                                jlong doc_ptr, jlong link_ptr) {
    auto *doc = reinterpret_cast<DocumentFile *>(doc_ptr);
    auto link = reinterpret_cast<FPDF_LINK>(link_ptr);
    FPDF_ACTION action = FPDFLink_GetAction(link);
    if (action == nullptr) {
        return nullptr;
    }
    size_t bufferLen = FPDFAction_GetURIPath(doc->pdfDocument, action, nullptr, 0);
    if (bufferLen <= 0) {
        return env->NewStringUTF("");
    }
    std::string uri;
    FPDFAction_GetURIPath(doc->pdfDocument, action, WriteInto(&uri, bufferLen), bufferLen);
    return env->NewStringUTF(uri.c_str());
}
extern "C"
JNIEXPORT jobject JNICALL
Java_io_github_petretiandrea_pdfium_PdfiumCore_nativePageCoordsToDevice(JNIEnv *env, jobject thiz,
                                                                        jlong page_ptr,
                                                                        jint start_x, jint start_y,
                                                                        jint size_x, jint size_y,
                                                                        jint rotate, jdouble page_x,
                                                                        jdouble page_y) {
    auto page = reinterpret_cast<FPDF_PAGE>(page_ptr);
    int deviceX, deviceY;

    FPDF_PageToDevice(page, start_x, start_y, size_x, size_y, rotate, page_x, page_y, &deviceX, &deviceY);

    jclass clazz = env->FindClass("android/graphics/Point");
    jmethodID constructorID = env->GetMethodID(clazz, "<init>", "(II)V");
    return env->NewObject(clazz, constructorID, deviceX, deviceY);
}